Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Infer the type of the result of calling typing.cast() #1076

Merged
merged 2 commits into from
Jul 30, 2021

Conversation

timmartin
Copy link
Contributor

@timmartin timmartin commented Jun 26, 2021

Description

Added an inference rule in brain_typing.py to infer the return type of typing.cast. So that:

from typing import cast

class A:
    pass

b = cast(A, x)  # Can infer that b is an instance of A

Type of Changes

Type
✨ New feature

Copy link
Member

@Pierre-Sassoulas Pierre-Sassoulas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the pull request ! It's pretty straightforward :)

@Pierre-Sassoulas Pierre-Sassoulas requested a review from cdce8p June 27, 2021 06:43
@Pierre-Sassoulas Pierre-Sassoulas added Brain 🧠 Needs a brain tip Enhancement ✨ Improvement to a component labels Jun 27, 2021
@Pierre-Sassoulas Pierre-Sassoulas added this to the 2.6.1 milestone Jun 27, 2021
Copy link
Member

@cdce8p cdce8p left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we should infer b as instance of A for b = cast(A, x). cast is used just for type hints and we probably shouldn't mix astroid inference and type hints. IMO the logical step would be to infer b as x since that's what cast does. @Pierre-Sassoulas What do you think?

ChangeLog Outdated Show resolved Hide resolved
astroid/brain/brain_typing.py Outdated Show resolved Hide resolved
astroid/brain/brain_typing.py Outdated Show resolved Hide resolved
astroid/brain/brain_typing.py Outdated Show resolved Hide resolved
tests/unittest_brain.py Outdated Show resolved Hide resolved
@timmartin
Copy link
Contributor Author

Thanks for the helpful review comments. In terms of what the inferred type of the result should be, maybe I'm fixing this in the wrong place. The original motivation was this report in Pylint; if we infer that typing.cast(A, x) has the same type as x then we won't fix this issue.

The way I look at it, typing.cast exists to provide information to make static checking better, so in an ideal world a linter could use the knowledge of the casted-to type to give better feedback. But does this mean that Pylint is the right place to apply this logic and not Astroid?

@cdce8p
Copy link
Member

cdce8p commented Jun 30, 2021

@timmartin I'm a bit hesitant with this one. Who makes sure A is truly the correct type? In an ideal world, x would be inferred correctly and we can just say that the result of cast equals x. Although I haven't looked at the issue in detail, it seems that there are two distinct issues:

  1. Currently, the result of cast is always Uninferrable
  2. wave.open might not be inferred correctly. This should be fixed separately by adding a brain module or writing a custom plugin.

One last thing, it's should be possible to use abstract classes for A just so the type checker is satisfied. However, that might prevent pylint from analyzing it further although it sometimes could.

@timmartin
Copy link
Contributor Author

The developer who adds the cast is responsible for checking that A is the right type. If they put the wrong annotation on, they'll get the wrong results from the type checker / linter. Garbage in, garbage out.

To me, the issue is that typing.cast is specifically designed to be used in the case where the correct type isn't / can't reasonably be inferred. It has no runtime semantics, it's purely a hint to the type checker. If x was being correctly inferred, there'd be no point in applying a cast.

I think you're right in theory that any problem that can be solved by a cast can be solved by sufficiently good type declarations / better inference. I think casting exists for the cases where that doesn't yet exist.

@cdce8p
Copy link
Member

cdce8p commented Jul 1, 2021

The issue I have with it is that cast is designed for type checking and not for inference. You could for example say that the type should be an abstract or protocol class. That will work perfectly fine for mypy, but won't offer any additional insides to us.

It might be an idea to add something like astroid.cast / pylint.cast that can be used explicitly for inference tips.

@Pierre-Sassoulas
Copy link
Member

It might be an idea to add something like astroid.cast / pylint.cast that can be used explicitly for inference tips.

It would add work for pylint users, what is the meaningful difference between typing for inference and typing for type/mypy ? I think cast does the work and have the advantages of being official and widespread. Also as @timmartin said, garbage in, garbage out, if you mislead mypy or pylint you get the error you deserve 😄

@cdce8p
Copy link
Member

cdce8p commented Jul 1, 2021

@Pierre-Sassoulas The point I'm trying to make here is that with inference we can already do better then what type checkers can. So using cast doesn't add anything for us. In contrast it might even prevent us from offering better suggestions.

@Pierre-Sassoulas
Copy link
Member

using cast doesn't add anything for us. In contrast it might even prevent us from offering better suggestions.

Well it's true that it might prevent us from offering better suggestions but only when the user made an explicit typing mistake. I think it add something for us if the inference fail. So maybe we should rely on cast if the inference failed and provide an option to be able to check cast ?

@timmartin
Copy link
Contributor Author

AIUI type checkers like mypy already do type inference, they can't rely on every variable having its type statically declared. Is there a class of inference that Astroid does that wouldn't be available in a sufficiently smart type checker?

I can certainly see how there will be cases in the real world where the cast may be needed for mypy and not for Pylint, say. That would make it annoying if adding the needed cast helped mypy but broke Pylint. On the other hand, there will be cases where Astroid isn't (yet) making the inference and it would be useful to have the option of a cast.

It should be easy enough to make it infer the type of cast(A, x) as the type of x if it's inferable, otherwise an instance of A. I think that would avoid making inference worse in any case, although it still might miss the opportunity to do (say) a downcast when the variable is known to be an instance of a parent class. Is that a good solution here?

TBH I don't have a strong opinion here, I just picked this one up because I thought it would be an easy way to get used to the codebase. If it's controversial, I'm happy to leave this to someone else.

@Pierre-Sassoulas
Copy link
Member

@hippo91 do you have an opinion on this ?

@hippo91
Copy link
Contributor

hippo91 commented Jul 4, 2021

Well that is not an easy question. Thanks all for giving your point of view. IMHO the best option here is to infer the result of the cast as 'A' only if the inference of 'x' failed. Otherwise I am afraid we lose precious informations as @cdce8p mentioned it. But I am afraid there is not a real clear solution for this.

@cdce8p
Copy link
Member

cdce8p commented Jul 4, 2021

Thanks for your input @hippo91!
I agree with your point and like the suggestion. So +1 from me

@Pierre-Sassoulas Pierre-Sassoulas added this to the 2.6.3 milestone Jul 8, 2021
@Pierre-Sassoulas Pierre-Sassoulas requested a review from cdce8p July 8, 2021 20:29
Copy link
Member

@cdce8p cdce8p left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall! Just a few last comments

ChangeLog Outdated Show resolved Hide resolved
astroid/brain/brain_typing.py Outdated Show resolved Hide resolved
astroid/brain/brain_typing.py Outdated Show resolved Hide resolved
astroid/brain/brain_typing.py Outdated Show resolved Hide resolved
@timmartin timmartin force-pushed the brain-typing-cast branch from 4045b6e to 1d4a2a0 Compare July 10, 2021 20:14
@Pierre-Sassoulas Pierre-Sassoulas modified the milestones: 2.6.3, 2.7.0 Jul 13, 2021
@cdce8p
Copy link
Member

cdce8p commented Jul 14, 2021

@Pierre-Sassoulas I would like to include it in 2.6.3 if that's fine with you. It will improve inference and thus help solve some issue.

@Pierre-Sassoulas Pierre-Sassoulas removed this from the 2.7.0 milestone Jul 14, 2021
@timmartin timmartin force-pushed the brain-typing-cast branch from cf8c4c8 to e791ed1 Compare July 16, 2021 15:49
@cdce8p
Copy link
Member

cdce8p commented Jul 16, 2021

@cdce8p Good catch. Is it reasonable to report uninferable in both cases? I can see how both cases could in principle be handled, but I don't know what infrastructure we already have for parsing type expression strings and working with typevars.

@timmartin Either that or raising UseInferenceDefault (which should ultimately also result in Uninferable).

@timmartin
Copy link
Contributor Author

I think my latest change deals with both cases. I'm not totally sure in the typevar case; in my testing it appears that the typevar just gets inferred to Uninferable, which we can handle easily. I don't know if this will suffice in general though, if we improve typevar inference presumably we could regress this functionality.

Copy link
Member

@cdce8p cdce8p left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Astroid does have some (albeit limited) inference for TypeVar (see below). The issue for us here is that it basically returns an empty class with the bare minimum and doesn't account for and constraints or bounds on the original TypeVar.

Just from a quick view, I fear we won't be able to easily find out if a type is a TypeVar or not. That unfortunately will bring us back to the point I made at the beginning: It's probably best to only infer cast as the variable itself even if it's Uninferable.

If you have another idea, I would be open to testing that out.

https://github.com/PyCQA/astroid/blob/a7e55b4caefd532d404ac9429f490edcc5ad7d5a/astroid/brain/brain_typing.py#L114-L128

ChangeLog Outdated
@@ -21,6 +20,8 @@ Release date: TBA

Closes PyCQA/pylint#4671

* Added support to infer return type of typing.cast()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Added support to infer return type of typing.cast()
* Added support to infer return type of ``typing.cast()``

The ChangeLog ist rst formatted.

"""Casting to a typevar is not currently inferred"""
node = builder.extract_node(
"""
from typing import cast
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
from typing import cast
from typing import cast, TypeVar

astroid/brain/brain_typing.py Outdated Show resolved Hide resolved
@timmartin
Copy link
Contributor Author

Just from a quick view, I fear we won't be able to easily find out if a type is a TypeVar or not. That unfortunately will bring us back to the point I made at the beginning: It's probably best to only infer cast as the variable itself even if it's Uninferable.

That's a shame. I'm a little puzzled though, because I was assuming that astroid would either successfully determine that the argument was a TypeVar, or else it would be uninferable. Are you saying that TypeVars are handled in a way that makes it impossible to distinguish a class from a typevar?

@cdce8p
Copy link
Member

cdce8p commented Jul 18, 2021

That's a shame. I'm a little puzzled though, because I was assuming that astroid would either successfully determine that the argument was a TypeVar, or else it would be uninferable. Are you saying that TypeVars are handled in a way that makes it impossible to distinguish a class from a typevar?

At the moment that is the case. Keep in mind though that this behavior is completely fine since astroid doesn't use type hints for inference tips. We just need to make sure they have some basic properties. There are probably some more cases that could cause issues: NewType, Protocol, Generic.

@Pierre-Sassoulas Pierre-Sassoulas modified the milestones: 2.6.3, 2.6.4, 2.6.5 Jul 19, 2021
@timmartin
Copy link
Contributor Author

If that's the case then it's probably best to do as you suggest and just infer from the second argument.

Will astroid differentiate between classes and typevars in the future? It seems like it would be necessary to catch some errors e.g. that typevars can't be constructed.

Looking at the typevar code, is there any way to distinguish based on metaclass? It looks like typevars are given a custom metaclass; if we could only infer cast for classes with a default metaclass that might be a small win. I fiddled around for a while but I couldn't come up with anything that didn't feel like a kludge, though.

@cdce8p
Copy link
Member

cdce8p commented Jul 20, 2021

If that's the case then it's probably best to do as you suggest and just infer from the second argument.

I would probably prefer that.

Will astroid differentiate between classes and typevars in the future? It seems like it would be necessary to catch some errors e.g. that typevars can't be constructed.

Maybe, there are multiple issues though. First you need someone to add it, then to review it, and most important someone to maintain it. (1) might be possible, but the other two unlikely. As it currently is, I too struggle sometimes to understand a change completely. That's the cost of good, but really old legacy code. Regarding typing in particular, I'm happy that most major issues are solved now. It has taken us moths to do that at the start of the year. (Also a reason why I prefer the safe route here.)

Looking at the typevar code, is there any way to distinguish based on metaclass? It looks like typevars are given a custom metaclass; if we could only infer cast for classes with a default metaclass that might be a small win. I fiddled around for a while but I couldn't come up with anything that didn't feel like a kludge, though.

You could try next(node.args[0].infer()).metaclass(), but again I would probably suggest to leave it be even it might technically be possible. I found the two issues after only a short test run. Don't know what other will come up with once it is released. Usually you have to decide if the feature you are adding is worth the additional bugs it might create. I'm just really skeptical about that.

I know, that's probably not the answer you were hoping for but that's how I feel about it. I hope you can understand it.

@Pierre-Sassoulas Pierre-Sassoulas modified the milestones: 2.6.5, 2.6.6 Jul 21, 2021
@timmartin timmartin force-pushed the brain-typing-cast branch from 4bd6ebe to 9e51d37 Compare July 22, 2021 22:07
@timmartin
Copy link
Contributor Author

I think my last change does what's required here. Should I change the tags on this PR to indicate that it's ready for re-review?

@Pierre-Sassoulas
Copy link
Member

@timmartin there's a little "reload" button in the reviewers so you can ask for a new review that way. :)

@Pierre-Sassoulas Pierre-Sassoulas self-requested a review July 29, 2021 16:03
@cdce8p cdce8p self-requested a review July 29, 2021 16:04
@Pierre-Sassoulas
Copy link
Member

Capture d’écran de 2021-07-29 18-03-41

Copy link
Member

@cdce8p cdce8p left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the ping. I must have missed your last push.
As for the PR, I committed some last changes (I've explained the reasoning behind them below).

It looks good now.
Thank you @timmartin 🐬

Comment on lines +373 to +374
if not isinstance(node.func, (Name, Attribute)):
raise UseInferenceDefault
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically this is covered by _looks_like_typing_cast but type checker don't know about that.

Comment on lines +1826 to +1827
assert isinstance(inferred, nodes.Const)
assert inferred.value == 42
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually it's a bit easier to test for a const value.

@cdce8p cdce8p added pylint-tested PRs that don't cause major regressions with pylint and removed Waiting on author labels Jul 29, 2021
Copy link
Member

@Pierre-Sassoulas Pierre-Sassoulas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Thank you for being so persistent 👏 This will go in astroid 2.6.6 then pylint 2.10

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Brain 🧠 Needs a brain tip Enhancement ✨ Improvement to a component pylint-tested PRs that don't cause major regressions with pylint
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants